{ *************************************************************************** }

 { Audio Tools Library                                                         }
 { Class TMonkey - for manipulating with Monkey's Audio file information       }

 { http://mac.sourceforge.net/atl/                                             }
 { e-mail: macteam@users.sourceforge.net                                       }

 { Copyright (c) 2000-2002 by Jurgen Faul                                      }
 { Copyright (c) 2003-2005 by The MAC Team                                     }

 { Version 1.7 (April 2005) by Gambit                                          }
 {   - updated to unicode file access                                          }

 { Version 1.6 (11 April 2004) by Gambit                                       }
 {   - Added Ratio property again                                              }

 { Version 1.5 (22 August 2003) by MaDah                                       }
 {   - Added support for Monkey's Audio 3.98                                   }
 {   - Added/changed/removed some stuff                                        }

 { Version 1.4 (29 July 2002)                                                  }
 {   - Correction for calculating of duration                                  }

 { Version 1.1 (11 September 2001)                                             }
 {   - Added property Samples                                                  }
 {   - Removed WAV header information                                          }

 { Version 1.0 (7 September 2001)                                              }
 {   - Support for Monkey's Audio files                                        }
 {   - Class TID3v1: reading & writing support for ID3v1 tags                  }
 {   - Class TID3v2: reading & writing support for ID3v2 tags                  }
 {   - Class TAPEtag: reading & writing support for APE tags                   }

 { This library is free software; you can redistribute it and/or               }
 { modify it under the terms of the GNU Lesser General Public                  }
 { License as published by the Free Software Foundation; either                }
 { version 2.1 of the License, or (at your option) any later version.          }

 { This library is distributed in the hope that it will be useful,             }
 { but WITHOUT ANY WARRANTY; without even the implied warranty of              }
 { MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU           }
 { Lesser General Public License for more details.                             }

 { You should have received a copy of the GNU Lesser General Public            }
 { License along with this library; if not, write to the Free Software         }
 { Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA   }

{ *************************************************************************** }

unit Monkey;

interface

uses
  Classes, SysUtils, ID3v1, ID3v2, APEtag, TntClasses;

const
  { Compression level codes }
  MONKEY_COMPRESSION_FAST      = 1000;    { Fast (poor) }
  MONKEY_COMPRESSION_NORMAL    = 2000;    { Normal (good) }
  MONKEY_COMPRESSION_HIGH      = 3000;    { High (very good) }
  MONKEY_COMPRESSION_EXTRA_HIGH = 4000;    { Extra high (best) }
  MONKEY_COMPRESSION_INSANE    = 5000;    { Insane }
  MONKEY_COMPRESSION_BRAINDEAD = 6000;    { BrainDead }
  { Compression level names }
  MONKEY_COMPRESSION: array [0..6] of string =
    ('Unknown', 'Fast', 'Normal', 'High', 'Extra High', 'Insane', 'BrainDead');

  { Format flags, only for Monkey's Audio <= 3.97 }
  MONKEY_FLAG_8_BIT  = 1;    // Audio 8-bit
  MONKEY_FLAG_CRC    = 2;    // New CRC32 error detection
  MONKEY_FLAG_PEAK_LEVEL = 4;    // Peak level stored
  MONKEY_FLAG_24_BIT = 8;    // Audio 24-bit
  MONKEY_FLAG_SEEK_ELEMENTS = 16;   // Number of seek elements stored
  MONKEY_FLAG_WAV_NOT_STORED = 32;   // WAV header not stored

  { Channel mode names }
  MONKEY_MODE: array [0..2] of string =
    ('Unknown', 'Mono', 'Stereo');

type
  { Class TMonkey }
  TMonkey = class (TObject)
  private
    { Private declarations }
    FValid:     boolean;
    // Stuff loaded from the header:
    FVersion:   integer;
    FVersionStr: string;
    FChannels:  integer;
    FSampleRate: integer;
    FBits:      integer;
    FPeakLevel: longword;
    FPeakLevelRatio: double;
    FTotalSamples: int64;
    FBitrate:   double;
    FDuration:  double;
    FCompressionMode: integer;
    FCompressionModeStr: string;
    // FormatFlags, only used with Monkey's <= 3.97
    FFormatFlags: integer;
    FHasPeakLevel: boolean;
    FHasSeekElements: boolean;
    FWavNotStored: boolean;
    // Tagging
    FID3v1:     TID3v1;
    FID3v2:     TID3v2;
    FAPEtag:    TAPEtag;

    FFileSize: int64;

    procedure FResetData;

    function FGetRatio: double;

  public
    { Public declarations }
    constructor Create;                                       { Create object }
    destructor Destroy; override;                            { Destroy object }

    function ReadFromFile(const FileName: WideString): boolean; { Load header }

    property FileSize: int64 Read FFileSize;
    property Valid: boolean Read FValid;
    property Version: integer Read FVersion;
    property VersionStr: string Read FVersionStr;
    property Channels: integer Read FChannels;
    property SampleRate: integer Read FSamplerate;
    property Bits: integer Read FBits;
    property Bitrate: double Read FBitrate;
    property Duration: double Read FDuration;
    property PeakLevel: longword Read FPeakLevel;
    property PeakLevelRatio: double Read FPeakLevelRatio;
    property TotalSamples: int64 Read FTotalSamples;
    property CompressionMode: integer Read FCompressionMode;
    property CompressionModeStr: string Read FCompressionModeStr;
    // FormatFlags, only used with Monkey's <= 3.97
    property FormatFlags: integer Read FFormatFlags;
    property HasPeakLevel: boolean Read FHasPeakLevel;
    property HasSeekElements: boolean Read FHasSeekElements;
    property WavNotStored: boolean Read FWavNotStored;
    // Tagging
    property ID3v1: TID3v1 Read FID3v1;                    { ID3v1 tag data }
    property ID3v2: TID3v2 Read FID3v2;                    { ID3v2 tag data }
    property APEtag: TAPEtag Read FAPEtag;                   { APE tag data }

    property Ratio: double Read FGetRatio;          { Compression ratio (%) }
  end;

implementation

type
  { Real structure of Monkey's Audio header }
  // common header for all versions
  APE_HEADER = packed record
    cID:      array[0..3] of byte;        // should equal 'MAC '
    nVersion: word;                       // version number * 1000 (3.81 = 3810)
  end;
  // old header for <= 3.97
  APE_HEADER_OLD = packed record
    nCompressionLevel,               // the compression level
    nFormatFlags,                    // any format flags (for future use)
    nChannels: word;                 // the number of channels (1 or 2)
    nSampleRate,                     // the sample rate (typically 44100)
    nHeaderBytes,
    // the bytes after the MAC header that compose the WAV header
    nTerminatingBytes,               // the bytes after that raw data (for extended info)
    nTotalFrames,                    // the number of frames in the file
    nFinalFrameBlocks: longword;     // the number of samples in the final frame
    nInt:      integer;
  end;
  // new header for >= 3.98
  APE_HEADER_NEW = packed record
    nCompressionLevel: word;
    // the compression level (see defines I.E. COMPRESSION_LEVEL_FAST)
    nFormatFlags: word;
    // any format flags (for future use) Note: NOT the same flags as the old header!
    nBlocksPerFrame: longword;    // the number of audio blocks in one frame
    nFinalFrameBlocks: longword;    // the number of audio blocks in the final frame
    nTotalFrames: longword;    // the total number of frames
    nBitsPerSample: word;        // the bits per sample (typically 16)
    nChannels:   word;        // the number of channels (1 or 2)
    nSampleRate: longword;   // the sample rate (typically 44100)
  end;
  // data descriptor for >= 3.98
  APE_DESCRIPTOR = packed record
    padded:   word;                   // padding/reserved (always empty)
    nDescriptorBytes,
    // the number of descriptor bytes (allows later expansion of this header)
    nHeaderBytes,                    // the number of header APE_HEADER bytes
    nSeekTableBytes,                  // the number of bytes of the seek table
    nHeaderDataBytes,
    // the number of header data bytes (from original file)
    nAPEFrameDataBytes,              // the number of bytes of APE frame data
    nAPEFrameDataBytesHigh,          // the high order number of APE frame data bytes
    nTerminatingDataBytes: longword;
    // the terminating data of the file (not including tag data)
    cFileMD5: array[0..15] of byte;
    // the MD5 hash of the file (see notes for usage... it's a littly tricky)
  end;

{ ********************** Private functions & procedures ********************* }

procedure TMonkey.FResetData;
begin
  { Reset data }
  FValid     := False;
  FVersion   := 0;
  FVersionStr := '';
  FChannels  := 0;
  FSampleRate := 0;
  FBits      := 0;
  FPeakLevel := 0;
  FPeakLevelRatio := 0.0;
  FTotalSamples := 0;
  FBitrate   := 0.0;
  FDuration  := 0.0;
  FCompressionMode := 0;
  FCompressionModeStr := '';
  FFormatFlags := 0;
  FHasPeakLevel := False;
  FHasSeekElements := False;
  FWavNotStored := False;
  FFileSize  := 0;
  FID3v1.ResetData;
  FID3v2.ResetData;
  FAPEtag.ResetData;
end;

{ ********************** Public functions & procedures ********************** }

constructor TMonkey.Create;
begin
  { Create object }
  inherited;
  FID3v1  := TID3v1.Create;
  FID3v2  := TID3v2.Create;
  FAPEtag := TAPEtag.Create;
  FResetData;
end;

{ --------------------------------------------------------------------------- }

destructor TMonkey.Destroy;
begin
  { Destroy object }
  FID3v1.Free;
  FID3v2.Free;
  FAPEtag.Free;
  inherited;
end;

{ --------------------------------------------------------------------------- }

function TMonkey.ReadFromFile(const FileName: WideString): boolean;
var
  f:   TTntFileStream;
  APE: APE_HEADER;          // common header
  APE_OLD: APE_HEADER_OLD;  // old header   <= 3.97
  APE_NEW: APE_HEADER_NEW;  // new header   >= 3.98
  APE_DESC: APE_DESCRIPTOR; // extra header >= 3.98
  BlocksPerFrame: integer;
  LoadSuccess: boolean;
  TagSize: integer;
begin
  Result := False;
  FResetData;
  // load tags first
  FID3v2.ReadFromFile(FileName);
  FID3v1.ReadFromFile(FileName);
  FAPEtag.ReadFromFile(FileName);
  // calculate total tag size
  TagSize := 0;
  if FID3v1.Exists then
    Inc(TagSize, 128);
  if FID3v2.Exists then
    Inc(TagSize, FID3v2.Size);
  if FAPEtag.Exists then
    Inc(TagSize, FAPETag.Size);
  // begin reading data from file
  LoadSuccess := False;
  f := nil;
  try
    try
      f := TTntFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
      FFileSize := f.Size;
      // seek past id3v2-tag
      if FID3v2.Exists then
        begin
        f.Seek(FID3v2.Size, soFromBeginning);
        end;
      // Read APE Format Header
      fillchar(APE, sizeof(APE), 0);
      if (f.Read(APE, sizeof(APE)) = sizeof(APE)) and
        (StrLComp(@APE.cID[0], 'MAC ', 4) = 0) then
        begin
        FVersion := APE.nVersion;
        Str(FVersion / 1000: 4: 2, FVersionStr);
        // Load New Monkey's Audio Header for version >= 3.98
        if APE.nVersion >= 3980 then
          begin
          fillchar(APE_DESC, sizeof(APE_DESC), 0);
          if (f.Read(APE_DESC, sizeof(APE_DESC)) = sizeof(APE_DESC)) then
            begin
            // seek past description header
            if APE_DESC.nDescriptorBytes <> 52 then
              f.Seek(APE_DESC.nDescriptorBytes - 52, soFromCurrent);
            // load new ape_header
            if APE_DESC.nHeaderBytes > sizeof(APE_NEW) then
              APE_DESC.nHeaderBytes := sizeof(APE_NEW);
            fillchar(APE_NEW, sizeof(APE_NEW), 0);
            if (longword(f.Read(APE_NEW, APE_DESC.nHeaderBytes)) =
              APE_DESC.nHeaderBytes) then
              begin
              // based on MAC SDK 3.98a1 (APEinfo.h)
              FSampleRate := APE_NEW.nSampleRate;
              FChannels := APE_NEW.nChannels;
              FFormatFlags := APE_NEW.nFormatFlags;
              FBits := APE_NEW.nBitsPerSample;
              FCompressionMode := APE_NEW.nCompressionLevel;
              // calculate total uncompressed samples
              if APE_NEW.nTotalFrames > 0 then
                begin
                FTotalSamples :=
                  int64(APE_NEW.nBlocksPerFrame) * int64(APE_NEW.nTotalFrames - 1) +
                  int64(APE_NEW.nFinalFrameBlocks);
                end;
              LoadSuccess := True;
              end;
            end;
          end
        else
          begin
          // Old Monkey <= 3.97
          fillchar(APE_OLD, sizeof(APE_OLD), 0);
          if (f.Read(APE_OLD, sizeof(APE_OLD)) = sizeof(APE_OLD)) then
            begin
            FCompressionMode := APE_OLD.nCompressionLevel;
            FSampleRate := APE_OLD.nSampleRate;
            FChannels := APE_OLD.nChannels;
            FFormatFlags := APE_OLD.nFormatFlags;
            FBits := 16;
            if APE_OLD.nFormatFlags and MONKEY_FLAG_8_BIT <> 0 then
              FBits := 8;
            if APE_OLD.nFormatFlags and MONKEY_FLAG_24_BIT <> 0 then
              FBits := 24;
            FHasSeekElements :=
              APE_OLD.nFormatFlags and MONKEY_FLAG_PEAK_LEVEL <> 0;
            FWavNotStored :=
              APE_OLD.nFormatFlags and MONKEY_FLAG_SEEK_ELEMENTS <> 0;
            FHasPeakLevel :=
              APE_OLD.nFormatFlags and MONKEY_FLAG_WAV_NOT_STORED <> 0;
            if FHasPeakLevel then
              begin
              FPeakLevel      := APE_OLD.nInt;
              FPeakLevelRatio := (FPeakLevel / (1 shl FBits) / 2.0) * 100.0;
              end;
            // based on MAC_SDK_397 (APEinfo.cpp)
            if (FVersion >= 3950) then
              BlocksPerFrame := 73728 * 4
            else
            if (FVersion >= 3900) or ((FVersion >= 3800) and
              (APE_OLD.nCompressionLevel = MONKEY_COMPRESSION_EXTRA_HIGH)) then
              BlocksPerFrame := 73728
            else
              BlocksPerFrame := 9216;
            // calculate total uncompressed samples
            if APE_OLD.nTotalFrames > 0 then
              begin
              FTotalSamples :=
                int64(APE_OLD.nTotalFrames - 1) * int64(BlocksPerFrame) +
                int64(APE_OLD.nFinalFrameBlocks);
              end;
            LoadSuccess := True;
            end;
          end;
        if LoadSuccess then
          begin
          // compression profile name
          if ((FCompressionMode mod 1000) = 0) and (FCompressionMode <= 6000) then
            begin
            FCompressionModeStr := MONKEY_COMPRESSION[FCompressionMode div 1000];
            end
          else
            begin
            FCompressionModeStr := IntToStr(FCompressionMode);
            end;
          // length
          if FSampleRate > 0 then
            FDuration := FTotalSamples / FSampleRate;
          // average bitrate
          if FDuration > 0 then
            FBitrate := (FFileSize - int64(TagSize)) * 8.0 / (FDuration / 1000.0);
          // some extra sanity checks
          FValid := (FBits > 0) and (FSampleRate > 0) and
            (FTotalSamples > 0) and (FChannels > 0);
          Result := FValid;
          end;
        end;
    finally
      f.Free;
      end;
  except
    end;
end;

{ --------------------------------------------------------------------------- }

function TMonkey.FGetRatio: double;
begin
  { Get compression ratio }
  if FValid then
    Result := FFileSize / (FTotalSamples * (FChannels * FBits / 8) + 44) * 100
  else
    Result := 0;
end;

{ --------------------------------------------------------------------------- }

end.
